package han.chensing.latos.maker.windows.widget.timeline;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.g2d.Batch;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.InputListener;
import com.badlogic.gdx.scenes.scene2d.ui.ButtonGroup;
import com.badlogic.gdx.scenes.scene2d.ui.TextButton;
import com.badlogic.gdx.scenes.scene2d.ui.TextField;
import com.badlogic.gdx.scenes.scene2d.ui.Widget;
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
import com.badlogic.gdx.utils.Array;
import han.chensing.latos.common.level.content.EditorConfig;
import han.chensing.latos.common.level.content.objects.CommonBPM;
import han.chensing.latos.common.level.content.objects.CommonLine;
import han.chensing.latos.common.level.content.objects.CommonNote;
import han.chensing.latos.common.level.content.objects.CommonObject;
import han.chensing.latos.common.level.content.timeLine.TimeLineEventSet;
import han.chensing.latos.common.level.content.timeLine.TimeLineRow;
import han.chensing.latos.common.level.content.timeLine.events.TimeLineEvent;
import han.chensing.latos.common.level.content.timeLine.events.TimeLineMotionEvent;
import han.chensing.latos.common.level.content.timeLine.frame.BaseFrame;
import han.chensing.latos.common.level.content.timeLine.frame.FrameGeneral;
import han.chensing.latos.common.level.content.timeLine.frame.GeneralEventTypes;
import han.chensing.latos.common.level.function.ListReference;
import han.chensing.latos.common.level.function.TimeObject;
import han.chensing.latos.common.level.util.*;
import han.chensing.latos.maker.GameMain;
import han.chensing.latos.maker.V;
import han.chensing.latos.maker.res.Res;
import han.chensing.latos.maker.windows.dialog.base.BaseDialog;
import han.chensing.latos.maker.windows.menus.popup.RightClickMenu;
import han.chensing.latos.maker.windows.widget.ShortCutWidget;
import space.earlygrey.shapedrawer.ShapeDrawer;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 * To be frank
 * I really took much time on this class
 * And it just like a shit now
 * Hmm... I think I should refactor it sooner or later
 * At 2024/1/12 20:10
 * <p></p>
 * Did some small refactor
 * At 2024/2/19 13:49
 * <p></p>
 * Dynamic bpm added ^v^
 * At 2024/3/18 20:38
 * <p></p>
 * This class is longer than my expected
 * I think I shou put some method to other class to reduce its length
 * At 2024/9/30
 */
@SuppressWarnings("DefaultLocale")
public class TimeLineWidget extends Widget {

    TimeLineWidgetUtil timeLineWidgetUtil=new TimeLineWidgetUtil();
    BeatUtil beatUtil =new BeatUtil(()->CommonBPM.getEventSet(V.project.getLevelData()));

    AdsorbMode adsorbMode=AdsorbMode.None;
    RulerType rulerType=RulerType.None;

    float scrollHorizontalPosition=0.f;
    float scrollVerticalPosition=0.f;
    float scrollHorizontalFade=0.f;
    float scrollVerticalFade=0.f;
    float nowTime=0;
    float speed=100f;
    float trackSelectFrom;
    float trackSelectTo;
    TimeLineWidgetUtil.Selection nowSelection;
    BaseFrame<?> nowKeyFrame;

    float oriSpeed=speed;
    float tx,ty;
    float tempScrollHorizontalPosition=0.f;
    float tempScrollVerticalPosition=0.f;
    float tempScrollHorizontalFade=0.f;
    float tempScrollVerticalFade=0.f;
    AdsorbMode adsorbModeBeforeAlt=AdsorbMode.None;
    boolean altHandled=false;
    float tTime;//FIXME Test

    final float itemsPerScreen =10f;
    final float scrollBarWidthOrHeight = 30f;
    final float scrollBarWide = 10f;
    final float leftWidth=170f;
    final float expandIconJudgeWidth=15f;

    float x;
    float y;
    float height;
    float width;
    ShapeDrawer shapeDrawer =GameMain.shapeDrawer;
    BitmapFont fontDeng;

    boolean hasRightClickMenu=false;
    boolean freezeNowTimeUpdate=false;
    boolean selecting=false;

    Color keyFrameColor =new Color(0x009EEAFF);
    Color eventBlockColor =new Color(0x99D9EAFF);
    Color nowTimeLineColor =new Color(0xFF0000FF);
    Color selectLineColor =new Color(0x0000FFFF);
    Color expandTriangleColor=new Color(0xFFFFFFFF);
    Color generalObjectBackgroundColor =new Color(0x5F5F5FFF);
    Color generalEventSetBackgroundColor =new Color(0x3F3F3FFF);
    Color editorEventSetBackgroundColor=new Color(0x5C2B5DFF);
    Color itemFrameworkColor=new Color(0x7F7F7FFF);
    Color scrollBarFadeColorHorizontal=new Color(0x70707000);
    Color scrollBarFadeColorVertical=new Color(0x70707000);

    int[] buttonMapping=new int[10];
    List<Object[]> alignmentList;
    RightClickMenu.RightClickMenuItem[] rightClickMenuItems;

    TextField nowTimeField;
    TextButton showEventNameCheck;
    TextButton beatModeCheck;
    ShortCutWidget shortCutWidget;
    ButtonGroup<TextButton> adsorbButtonGroup;
    TimeLineWidgetUpdateInterface timeLineWidgetUpdateInterface;

    private class WidgetInputListener extends InputListener{
        boolean moveVertical =false, moveHorizontal =false;

        @Override
        public void touchDragged(InputEvent event, float cx, float cy, int pointer) {
            super.touchDragged(event, cx, cy, pointer);
            if (moveHorizontal) tempScrollHorizontalPosition= MathUtil.range(0,(cx-x)/(width-scrollBarWidthOrHeight),1);
            else if (moveVertical) tempScrollVerticalPosition=MathUtil.range(0,1-(cy-y)/(height-scrollBarWidthOrHeight)-0.15f,1);
            else
                switch (buttonMapping[pointer]) {
                    case Input.Buttons.MIDDLE -> speed = Math.max(50f, oriSpeed + (cx - tx));
                    case Input.Buttons.LEFT -> {
                        updateNowTimeFromPosition(cx);
                        //updateFrameToParent();
                        trackSelectTo = nowTime;
                    }
                    case Input.Buttons.RIGHT -> {
                    }
                }
        }
        @Override
        public void touchUp(InputEvent event, float x, float y, int pointer, int button) {
            super.touchUp(event, x, y, pointer, button);
            if (buttonMapping[pointer]==Input.Buttons.RIGHT&&!(moveHorizontal || moveVertical)) {
                updateNowTimeFromPosition(x);
                selecting=false;
                V.project.getMusic().play();
                V.project.getMusic().setPosition(trackSelectTo);
                V.project.getMusic().pause();
            }
            moveVertical = moveHorizontal =false;
            buttonMapping[pointer]=-1;
        }
        @Override
        public boolean touchDown(InputEvent event, float cx, float cy, int pointer, int button) {
            BaseDialog parent = (BaseDialog) getParent();
            V.container.getStage().setScrollFocus(TimeLineWidget.this);
            float wx = parent.getX();
            float wy = parent.getY();
            tx=cx;
            ty=cy;
            oriSpeed=speed;
            buttonMapping[pointer]=button;
            switch (button) {
                case Input.Buttons.LEFT -> {
                    checkMoveVertical(cx,cy);
                    checkMoveHorizontal(cx,cy);
                    if (moveVertical || moveHorizontal) break;
                    boolean clickedExpandIcon = checkClickExpandIcon(cx, cy);
                    updateSelectIndexFromAbsolutePosition(cy);
                    if (clickedExpandIcon){
                        performExpandOrNot();
                        break;
                    }
                    updateNowTimeFromPosition(cx);
                    trackSelectTo = trackSelectFrom = nowTime;
                    selecting = true;
                }
                case Input.Buttons.RIGHT -> {//FIXME
                    if ((moveVertical || moveHorizontal)) break;
                    updateNowTimeFromPosition(cx);
                    trackSelectTo=nowTime;
                    RightClickMenu newRightClickMenu = RightClickMenu.getNewRightClickMenu(
                            wx+x+cx,wy+y+cy,null,rightClickMenuItems
                    );
                    newRightClickMenu.setCloseListener(lostFocus -> hasRightClickMenu=false);
                    hasRightClickMenu=true;
                    V.container.addWindow(newRightClickMenu);
                    /*updateFrameToParent();
                    updateEventToParent();
                    updateKeyFramesToParent();*/
                }
            }
            return true;
        }

        @Override
        public boolean mouseMoved(InputEvent event, float cx, float cy) {
            tempScrollVerticalFade=isInMoveVertical(cx,cy)?1:0;
            tempScrollHorizontalFade=isInMoveHorizontal(cx,cy)?1:0;
            return super.mouseMoved(event, cx, cy);
        }

        @Override
        public boolean scrolled(InputEvent event, float x, float y, float amountX, float amountY) {
            boolean fast = shortCutWidget.isKeyPressed(ShortCutWidget.NUM_SHIFT);
            boolean slow=shortCutWidget.isKeyPressed(ShortCutWidget.NUM_ALT);
            float side = amountY==0?1:amountY / Math.abs(amountY);
            float height = getHeight();
            boolean vertical = shortCutWidget.isKeyPressed(ShortCutWidget.NUM_CONTROL);
            if (vertical){
                int delta=fast?5:3;
                delta=slow?1:delta;
                delta*=Math.abs((int)amountY);
                float unitHeight=(height-scrollBarWide) / itemsPerScreen;
                float itemTotalHeight = timeLineWidgetUtil.getItemTotalHeight(unitHeight);
                float perUnitPercent=unitHeight/itemTotalHeight;
                tempScrollVerticalPosition+=perUnitPercent*delta*side;
                tempScrollVerticalPosition=MathUtil.range(0,tempScrollVerticalPosition,1);
                tempScrollVerticalFade=1;
            }else {
                float delta = fast ? 8f : 2f;
                delta=slow?0.5f:delta;
                delta*=Math.abs(amountY);
                float musicLength = V.project.getMusicLength();
                tempScrollHorizontalPosition =
                        ((scrollHorizontalPosition * musicLength) + delta * side) / musicLength;
                tempScrollHorizontalPosition = MathUtil.range(0, tempScrollHorizontalPosition, 1);
                tempScrollHorizontalFade=1;
            }
            return super.scrolled(event, x, y, amountX, amountY);
        }

        private boolean isInMoveHorizontal(float cx,float cy){
            Rectangle hr = new Rectangle(x, y, width, scrollBarWide);
            return hr.contains(cx + x, cy + y);
        }

        private void checkMoveHorizontal(float cx,float cy){
            moveHorizontal=isInMoveHorizontal(cx, cy);
        }

        private boolean isInMoveVertical(float cx,float cy){
            Rectangle vr = new Rectangle(x + width - scrollBarWide, y, scrollBarWide, height);
            return vr.contains(cx + x, cy + y);
        }

        private void checkMoveVertical(float cx,float cy){
            moveVertical=isInMoveVertical(cx, cy);
        }

        private boolean checkClickExpandIcon(float cx,float cy){
            Rectangle iconRect=new Rectangle(x,y,expandIconJudgeWidth,height);
            return iconRect.contains(x+cx,y+cy);
        }

        private void performExpandOrNot(){
            TimeLineWidgetUtil.Selection selection = nowSelection;
            if(selection.commonObject==null)return;
            if (selection.timeLineEventSetIndex!=-1)return;
            selection.commonObject.reverseConfigAsBoolean(EditorConfig.Expand,false);
            timeLineWidgetUtil.updateCachedShowedItems();
        }
    }

    public TimeLineWidget(
            TextField nowTimeField,
            TextButton showEventNameCheck,
            TextButton beatModeCheck,
            ButtonGroup<TextButton> adsorbButtonGroup,
            ShortCutWidget shortCutWidget,
            TimeLineWidgetUpdateInterface timeLineWidgetUpdateInterface){
        this.adsorbButtonGroup=adsorbButtonGroup;
        this.nowTimeField=nowTimeField;
        this.showEventNameCheck=showEventNameCheck;
        this.beatModeCheck=beatModeCheck;
        this.shortCutWidget=shortCutWidget;
        this.timeLineWidgetUpdateInterface = timeLineWidgetUpdateInterface;
        alignmentList= Collections.synchronizedList(new ArrayList<>());
        Arrays.fill(buttonMapping,-1);
        addListener(new WidgetInputListener());
        setUpNowTimeFieldListener();
        setUpRulerModeListener();
        setUpAdsorbModeGroupListener();
        setUpRightClickMenuItems();
    }

    public void init(){
        fontDeng= Res.get().get("fontDeng",BitmapFont.class);
        clearKeyFrameUpdates();
        if (timeLineWidgetUpdateInterface!=null){
            timeLineWidgetUpdateInterface.onMotionEventUpdated(null);
            timeLineWidgetUpdateInterface.onFrameInfoUpdated(new FrameGeneral());
        }
    }

    private void setUpNowTimeFieldListener(){
        nowTimeField.addListener(new InputListener(){
            @Override
            public boolean keyDown(InputEvent event, int keycode) {
                if (keycode==Input.Keys.ENTER||keycode==Input.Keys.NUMPAD_ENTER)
                    try {
                        String text = nowTimeField.getText();
                        String[] texts = text.split(",");
                        if (texts.length==1) {
                            nowTime = Float.parseFloat(texts[0]);
                        } else if (texts.length>=2){
                            float time1 = Float.parseFloat(texts[0]);
                            float time2 = Float.parseFloat(texts[1]);
                            nowTime=trackSelectTo=time2;
                            trackSelectFrom=time1;
                        }
                        freezeNowTimeUpdate=false;
                        return false;
                    }catch (Exception ignore){}
                freezeNowTimeUpdate=true;
                return super.keyDown(event, keycode);
            }
        });
    }

    private void setUpRightClickMenuItems() {
        rightClickMenuItems = new RightClickMenu.RightClickMenuItem[]{
                new RightClickMenu.RightClickMenuItem("Add key frame", ()->V.container.postRun(this::clickMenuItemAddKeyFrame)),
                new RightClickMenu.RightClickMenuItem("Add event", ()->V.container.postRun(this::clickMenuItemAddEvent)),
                new RightClickMenu.RightClickMenuItem("Remove event",()->V.container.postRun(this::clickMenuItemRemoveEvent)),
                new RightClickMenu.RightClickMenuItem("Remove key frame", ()->V.container.postRun(this::clickMenuItemRemoveKeyFrame))};
    }

    private void setUpRulerModeListener(){
        beatModeCheck.addListener(new ChangeListener() {
            @Override
            public void changed(ChangeEvent event, Actor actor) {
                rulerType=beatModeCheck.isChecked()?RulerType.Beat:RulerType.None;
            }
        });
    }



    @SuppressWarnings("GDXJavaUnsafeIterator")
    private void setUpAdsorbModeGroupListener(){
        Array<TextButton> buttons = adsorbButtonGroup.getButtons();
        for (TextButton button : buttons) {
            button.addListener(new ChangeListener() {
                @Override
                public void changed(ChangeEvent event, Actor actor) {
                    updateAdsorbMode();
                }
            });
        }
    }

    private void clickMenuItemAddKeyFrame(){
        TimeLineWidgetUtil.Selection nowSelected = getNowSelected();
        if (nowSelected==null) return;
        if (nowSelected.timeLineEventSetIndex==-1)return;
        TimeLineEventSet<?, ?> timeLineEventSet = nowSelected.timeLineEventSet;//TODO 修改nowTime
        if (nowSelected.isGeneral)
            timeLineEventSet.makeLineMoveGeneralKeyFrame(nowTime);
        else
            timeLineEventSet.makeEditorKeyFrame(nowTime);
    }

    private void clickMenuItemAddEvent(){
        TimeLineWidgetUtil.Selection nowSelected = getNowSelected();
        if (nowSelected==null) return;
        if (nowSelected.timeLineEventSetIndex==-1)return;
        TimeLineEventSet<?,?> timeLineEventSet = nowSelected.timeLineEventSet;
        if (nowSelected.isGeneral) timeLineEventSet.makeLineMoveGeneralEvent(nowTime);
    }

    private void clickMenuItemRemoveKeyFrame(){
        TimeLineWidgetUtil.Selection nowSelected = getNowSelected();
        if (nowSelected==null) return;
        if (nowSelected.timeLineEventSetIndex==-1)return;
        TimeLineEventSet<?,?> timeLineEventSet = nowSelected.timeLineEventSet;
        timeLineEventSet.deleteNearestKeyFrame(nowTime);
    }

    private void clickMenuItemRemoveEvent(){
        TimeLineWidgetUtil.Selection nowSelected = getNowSelected();
        if (nowSelected==null) return;
        if (nowSelected.timeLineEventSetIndex==-1)return;
        TimeLineEventSet<?,?> timeLineEventSet = nowSelected.timeLineEventSet;
        if (nowSelected.isGeneral) timeLineEventSet.deleteLineMoveEvent(nowTime);
    }

    private void updateAdsorbMode(){
        int checkedIndex = adsorbButtonGroup.getCheckedIndex();
        adsorbMode=switch (checkedIndex){
            case 1->AdsorbMode.Frame;
            case 2->AdsorbMode.Beat;
            case 3->AdsorbMode.Note;
            default -> AdsorbMode.None;
        };
    }

    private void updateAltAdsorbChange(){
        boolean altPressed = shortCutWidget.isKeyPressed(ShortCutWidget.NUM_ALT);
        if (altPressed&&!altHandled){
            altHandled=true;
            adsorbModeBeforeAlt=adsorbMode;
            adsorbButtonGroup.getButtons().get(1).toggle();
            return;
        }
        if (!altPressed&&altHandled){
            altHandled=false;
            adsorbButtonGroup.getButtons().get(0).toggle();
            adsorbMode=adsorbModeBeforeAlt;
        }
    }

    private void updateNowTime(){
        if(selecting)return;
        nowTime= V.project.getMusic().getPosition();
    }

    private void drawScrollBarAndOutLine(){
        scrollBarFadeColorHorizontal.a=scrollHorizontalFade;
        scrollBarFadeColorVertical.a=scrollVerticalFade;
        shapeDrawer.setColor(0,0,0,1);
        shapeDrawer.rectangle(x,y,width,height);
        shapeDrawer.setColor(1,1,1,1);
        shapeDrawer.filledRectangle(x,y,width,scrollBarWide, scrollBarFadeColorHorizontal);
        shapeDrawer.filledRectangle(x+width-scrollBarWide,y,scrollBarWide,height, scrollBarFadeColorVertical);
        shapeDrawer.filledRectangle(
                (scrollHorizontalPosition*width)/width*(width-scrollBarWidthOrHeight)+x,
                y,
                scrollBarWidthOrHeight,
                scrollBarWide);
        shapeDrawer.filledRectangle(
                x+width- scrollBarWide,
                (((1-scrollVerticalPosition)*height))/height*(height-scrollBarWidthOrHeight)+y,
                scrollBarWide,
                scrollBarWidthOrHeight);
    }

    private void drawTimeLine(Batch batch){
        timeLineWidgetUtil.checkCachedShowedItems();
        drawTimeLineFramework(batch);
        drawTimeLineFramesAndEvents(batch);
    }

    private void drawTimeLineFramesAndEvents(Batch batch){
        TimeLineWidgetUtil.Selection selection = nowSelection;
        if (selection==null)return;
        if (clipBegin(x+leftWidth,y+scrollBarWide,width-leftWidth,height-scrollBarWide)) {
            int absoluteIndex=0;
            for (CommonObject commonObject : V.project.getLevelData()) {
                boolean isExpand = commonObject.getConfigAsBoolean(EditorConfig.Expand, false);
                absoluteIndex++;
                if (!isExpand) continue;
                for (TimeLineEventSet<?,?> timeLineEventSet : commonObject.getTimeLineRow().getTimeLineEventSets()) {
                    drawTimeLineEvents(timeLineEventSet,absoluteIndex);
                    drawTimeLineFrames(timeLineEventSet,absoluteIndex);
                    absoluteIndex++;
                }
            }
            batch.flush();
            clipEnd();
        }
    }

    private void drawTimeLineEvents(TimeLineEventSet<?,?> timeLineEventSet,int k){
        int unitHeight=(int)Math.floor((height-scrollBarWide) / itemsPerScreen);
        float leftTime=fromAbsolutePositionToMusicTime(leftWidth);
        float rightTime=fromAbsolutePositionToMusicTime(width);
        float itemTotalHeight = timeLineWidgetUtil.getItemTotalHeight(unitHeight);
        float drawingStartY=height+(itemTotalHeight-height+scrollBarWide)*scrollVerticalPosition+unitHeight;
        float drawPosition=drawingStartY-unitHeight*(k+1);
        List<? extends TimeLineEvent<?>> timeLineEvents = timeLineEventSet.getTimeLineEvents();
        for (TimeLineEvent<?> timeLineEvent : timeLineEvents) {
            float fromFrameTime = timeLineEvent.getFromTime();
            float toFrameTime = timeLineEvent.getToTime();
            if (toFrameTime<=leftTime+1)continue;
            if (fromFrameTime>=rightTime-1)break;
            float startPosition=fromMusicTimeToAbsolutePosition(Math.max(leftTime,fromFrameTime));
            float endPosition=fromMusicTimeToAbsolutePosition(Math.min(rightTime,toFrameTime));
            shapeDrawer.filledRectangle(x+startPosition,y+drawPosition-unitHeight,
                    endPosition-startPosition,unitHeight,eventBlockColor);
        }
    }

    private void drawTimeLineFrames(TimeLineEventSet<?,?> timeLineEventSet,int k){
        int unitHeight=(int)Math.floor((height-scrollBarWide) / itemsPerScreen);
        float leftTime=fromAbsolutePositionToMusicTime(leftWidth);
        float rightTime=fromAbsolutePositionToMusicTime(width);
        float itemTotalHeight = timeLineWidgetUtil.getItemTotalHeight(unitHeight);
        float drawingStartY=height+(itemTotalHeight-height+scrollBarWide)*scrollVerticalPosition+unitHeight;
        float drawPosition=drawingStartY-unitHeight*(k+1);
        List<? extends BaseFrame<?>> keyFrames = timeLineEventSet.getKeyFrames();
        for (BaseFrame<?> keyFrame : keyFrames) {
            if (keyFrame.getTime()<=leftTime-1)continue;
            if (keyFrame.getTime()>=rightTime+1)break;
            float keyFramePosition = fromMusicTimeToAbsolutePosition(keyFrame.getTime());
            shapeDrawer.line(x+keyFramePosition,y+drawPosition,
                    x+keyFramePosition,y+drawPosition-unitHeight,keyFrameColor,9);
        }
    }

    private void drawTimeLineFramework(Batch batch){
        float unitHeight=(height-scrollBarWide) / itemsPerScreen;
        if(clipBegin(x,y+scrollBarWide,width,height-scrollBarWide)){
            float itemTotalHeight = timeLineWidgetUtil.getItemTotalHeight(unitHeight);
            float drawingStartY=height+(itemTotalHeight-height+scrollBarWide)*scrollVerticalPosition+unitHeight;
            int drawIndex=0;
            for (CommonObject commonObject : V.project.getLevelData()) {
                TimeLineRow timeLineRow = commonObject.getTimeLineRow();
                boolean isExpand = commonObject.getConfigAsBoolean(EditorConfig.Expand, false);
                float nowY=drawingStartY-drawIndex*unitHeight;
                shapeDrawer.filledRectangle(x, nowY,width,unitHeight, generalObjectBackgroundColor);
                shapeDrawer.rectangle(x, nowY,width,unitHeight,itemFrameworkColor);
                if (isExpand)
                    shapeDrawer.filledTriangle(
                            x+7,nowY+2/3f*unitHeight-2.4f,
                            x+17,nowY+2/3f*unitHeight-2.4f,
                            x+12f,nowY+1/3f*unitHeight+2.4f,expandTriangleColor);
                else
                    shapeDrawer.filledTriangle(
                            x+15,nowY+unitHeight/2f,
                            x+10,nowY+2/3f*unitHeight,
                            x+10,nowY+1/3f*unitHeight,expandTriangleColor);
                fontDeng.draw(batch,timeLineRow.getName(),
                        x+25, nowY+fontDeng.getCapHeight()/2f+unitHeight/2f);
                drawIndex++;
                if (isExpand) {
                    for (TimeLineEventSet<?,?> timeLineEventSet : commonObject.getTimeLineRow().getTimeLineEventSets()) {
                        Enum<?> type = timeLineEventSet.getType();
                        boolean isGeneral= ObjectUtil.isInEnum(GeneralEventTypes.values(),type.name());
                        nowY=drawingStartY-drawIndex*unitHeight;
                        shapeDrawer.filledRectangle(x, nowY,width,unitHeight,
                                isGeneral?generalEventSetBackgroundColor:editorEventSetBackgroundColor);
                        shapeDrawer.rectangle(x, nowY,width,unitHeight,itemFrameworkColor);
                        fontDeng.draw(batch, type.name(),
                                x+45, nowY+fontDeng.getCapHeight()/2f+unitHeight/2f);
                        drawIndex++;
                    }
                }
            }
            batch.flush();
            clipEnd();
        }
    }

    private void drawRuler(Batch batch){
        drawRulerCommon();
        switch (rulerType){
            case Beat -> drawBeatRuler(batch);
            case None -> drawNormalRuler(batch);
        }
    }

    private void drawRulerCommon(){
        shapeDrawer.line(x+leftWidth,y+scrollBarWide,x+leftWidth,y+height,Color.WHITE);
    }

    private void drawBeatRuler(Batch batch){
        BeatUtil.BeatQueryFunction beatQueryFunction= (time, nowBpm, relativeBeat) ->{
            float absolutePosition = fromMusicTimeToAbsolutePosition(time);
            if (!MathUtil.isInRange(leftWidth,absolutePosition,width))return false;
            fontDeng.draw(batch,String.valueOf(relativeBeat),x+absolutePosition,y+height+20f);
            shapeDrawer.line(x+absolutePosition,y,x+absolutePosition,y+height,Color.CYAN,2.5f);
            return true;
        };
        fontDeng.setColor(Color.CYAN);
        float leftTime=fromAbsolutePositionToMusicTime(leftWidth);
        beatUtil.queryBeatsForward(leftTime,beatQueryFunction);
        beatUtil.queryBeatsBackward(leftTime,beatQueryFunction);
        fontDeng.setColor(Color.WHITE);
    }

    private void drawNormalRuler(Batch batch){
        if (clipBegin(x+leftWidth,y+scrollBarWide,width-leftWidth,height+20-scrollBarWide)) {
            float offset = V.project.getLatosMetaData().getOffset();
            float offsetPosition = fromMusicTimeToAbsolutePosition(offset);
            if (MathUtil.isInRange(leftWidth - width/3f, offsetPosition, width + width/3f)) {
                fontDeng.draw(batch, "Offset", offsetPosition + x, 20 + height + y);
                shapeDrawer.line(offsetPosition + x, y + scrollBarWide,
                        offsetPosition + x, y + height);
            }
            float musicLength = V.project.getMusicLength();
            float musicTime = musicLength * scrollHorizontalPosition;
            float ceilMusicTime = (int) Math.ceil(musicTime);
            float rightTime = fromAbsolutePositionToMusicTime(width);
            for (float currentTime = ceilMusicTime + offset-1; currentTime <= rightTime; currentTime += 1) {
                if (MathUtil.isInRange(0, currentTime, offset)) continue;
                float absolutePositionX = fromMusicTimeToAbsolutePosition(currentTime);
                if (!MathUtil.isInRange(leftWidth - width/5f, absolutePositionX, width + width/5f)) continue;
                shapeDrawer.line(absolutePositionX + x, y + scrollBarWide,
                        absolutePositionX + x, y + height);
                fontDeng.draw(batch, String.valueOf (Math.round (currentTime - offset)), absolutePositionX + x, 20 + height + y);
            }
            batch.flush();
            clipEnd();
        }
    }

    private void drawNowTimeLine(){
        float nowTimePosition = fromMusicTimeToAbsolutePosition(nowTime);
        if (MathUtil.isInRange(leftWidth,nowTimePosition, width))
            shapeDrawer.line(nowTimePosition+x, y+scrollBarWide,
                    nowTimePosition+ x, y +20+ height,nowTimeLineColor);
    }

    private void drawSelection(){
        TimeLineWidgetUtil.Selection selection = nowSelection;
        if (selection==null)return;
        if (clipBegin(x,y+scrollBarWide,width,height-scrollBarWide)) {
            float fromPosition = fromMusicTimeToAbsolutePosition(trackSelectFrom);
            float toPosition = fromMusicTimeToAbsolutePosition(trackSelectTo);
            float realFrom = MathUtil.range(leftWidth, fromPosition, width);
            float realTo = MathUtil.range(leftWidth, toPosition, width);
            float unitHeight=(height-scrollBarWide) / itemsPerScreen;
            float itemTotalHeight = timeLineWidgetUtil.getItemTotalHeight(unitHeight);
            float drawingStartY=height+(itemTotalHeight-height+scrollBarWide)*scrollVerticalPosition+unitHeight;
            shapeDrawer.rectangle(x + realFrom, y+drawingStartY -(selection.exactIndex+2)*unitHeight,
                    realTo - realFrom, unitHeight, nowTimeLineColor);
            clipEnd();
        }
    }

    private void drawSelectedLine(Batch batch){
        TimeLineWidgetUtil.Selection selection = getNowSelected();
        if (selection==null)return;
        if(clipBegin(x,y+scrollBarWide,width,height-scrollBarWide)){
            float unitHeight = (height - scrollBarWide) / itemsPerScreen;
            int exactIndex = selection.exactIndex;
            float itemTotalHeight = timeLineWidgetUtil.getItemTotalHeight(unitHeight);
            float drawingStartY=height+(itemTotalHeight-height+scrollBarWide)*scrollVerticalPosition;
            float selectedLineLeftUp = drawingStartY -unitHeight *  (exactIndex+1);
            shapeDrawer.rectangle(x,selectedLineLeftUp+ y,leftWidth,unitHeight,selectLineColor);
            batch.flush();
            clipEnd();
        }
    }

    private void drawBox(){
        shapeDrawer.filledRectangle(x,y+scrollBarWide,leftWidth,height-scrollBarWide,Color.GRAY);
        shapeDrawer.filledRectangle(x+leftWidth,y,width-leftWidth,height,Color.DARK_GRAY);
    }
/*
    private void drawTineLineRowNotes(int lineNum,int num){
        if (!adsorbToNote)return;
        float x = getX();
        float y = getY();
        float width = getWidth();
        float height = getHeight();
        int unitHeight=(int)Math.floor(height / itemsPerScreen);
        float absY = num * unitHeight + y;
        float musicLength = V.project.getMusicLength();
        float musicTime = musicLength * scrollHorizontalPosition;
        ShapeDrawer shapeRenderer = GameMain.shapeRenderer;
        CommonLine commonLine = V.project.getLevelData().getLines().get(lineNum);
        ArrayList<CommonNote> commonNotes = commonLine.getNotes();
        for (CommonNote commonNote : commonNotes) {
            float absTime = commonNote.getBindSample().getAbsoluteTime();
            if (absTime<=musicTime)continue;
            if (absTime>=musicTime+(width-leftWidth)/speed)break;
            float absPosInTimeLine = getAbsPosInTimeLine(absTime);
            Color noteColor = CommonNote.getNoteColor(commonNote);
            shapeRenderer.rectangle(
                    absPosInTimeLine+ x,absY,3,unitHeight,
                    noteColor,noteColor,noteColor,noteColor);
        }
    }

    private float adsorbNowTime(float nowTime){
        if (adsorbToBeat){
            float offset = V.project.getLatosMetaData().getOffset();
            alignmentList.clear();
            alignmentList.add(new Object[]{1});
            Float nearestBeatTime = TimeUtil.getNearestBeatTime(nowTime,alignmentList);
            if (nearestBeatTime==null)return nowTime;
            return nearestBeatTime-offset;
        }
        if (adsorbToNote){
            int[] trueAndChildIndex = getTrueAndChildIndex();
            int bindLineNum = V.project.getLevelData().getTimeLine().getTimeLineRows().get(trueAndChildIndex[0]).getBindLineNum();
            CommonLine commonLine = V.project.getLevelData().getLines().get(bindLineNum);
            Float nearestNoteTime = TimeUtil.getNearestNoteTime(nowTime, commonLine.getNotes());
            if (nearestNoteTime==null)return nowTime;
            return nearestNoteTime;
        }
        return nowTime;
    }*/

    private FrameGeneral getNowLineFrame(){
        TimeLineWidgetUtil.Selection nowSelected = getNowSelected();
        if (nowSelected==null)return new FrameGeneral();
        return FrameUtil.getGeneralLineNowFrame(nowSelected.timeLineRow, nowTime);
    }

    private TimeLineEvent<?> getNowLineEvent(){
        TimeLineWidgetUtil.Selection nowSelected = getNowSelected();
        if (nowSelected==null)return null;
        if (nowSelected.timeLineEventSetIndex==-1)return null;
        return FrameUtil.getNowLineEvent(nowTime, nowSelected.timeLineEventSet);
    }

    private float adsorbNowTime(float musicTime){
        clearKeyFrameUpdates();
        return switch (adsorbMode){
            case Beat -> adsorbToBeat(musicTime);
            case Frame -> adsorbToKeyFrame(musicTime);
            case Note -> adsorbToNote(musicTime);
            default -> musicTime;
        };
    }

    private float adsorbToBeat(float time){
        tTime=time;
        float lastBeatTime = beatUtil.getNearBackBeatTime(time);
        float nextBeatTime = beatUtil.getNearFrontBeatTime(time);
        /*TimeLineEventSet<?,?> bpmTimeLineEventSet = getBpmTimeLineEventSet();
        if (bpmTimeLineEventSet==null)return 0;
        Frame lastBpmKeyFrame = FrameUtil.getLastKeyFrame(time, bpmTimeLineEventSet);
        if (lastBpmKeyFrame==null) return 0;
        float bpm=lastBpmKeyFrame.getBpm();
        float totalTime=time-lastBpmKeyFrame.getTime();
        float beats=secondToBeat(bpm,totalTime);
        float beatRound= (float) Math.round(beats);
        return beatToSecond(bpm,beatRound)+lastBpmKeyFrame.getTime();*/
        return Math.abs(lastBeatTime-time)<Math.abs(nextBeatTime-time)?lastBeatTime:nextBeatTime;
    }

    private float adsorbToKeyFrame(float time){
        TimeLineWidgetUtil.Selection nowSelected = getNowSelected();
        if (nowSelected==null)return 0;
        if (nowSelected.timeLineEventSetIndex==-1)return time;
        BaseFrame<?> nearestKeyFrame = FrameUtil.getNearestKeyFrame(time, nowSelected.timeLineEventSet);
        if (nearestKeyFrame==null)return 0;
        if (timeLineWidgetUpdateInterface!=null)
            timeLineWidgetUpdateInterface.onKeyFrameUpdated(nowKeyFrame=nearestKeyFrame);
        return nearestKeyFrame.getTime();
    }

    private float adsorbToNote(float time){
        TimeLineWidgetUtil.Selection nowSelected = getNowSelected();
        if (nowSelected==null)return 0;
        if (!(nowSelected.commonObject instanceof CommonLine commonLine))return 0;
        ArrayList<CommonNote> commonNotes = commonLine.getCommonNotes();
        TimeObject nearestTimeObject = TimeUtil.getNearestHasTime(new ListReference<>() {
            @Override
            public TimeObject get(int index) {
                return ()->commonNotes.get(index).getStartTime();
            }
            @Override
            public int size() {
                return commonNotes.size();
            }
        }, time);
        if (nearestTimeObject ==null)return 0;
        return nearestTimeObject.getTime();
    }

    private void updateNowTimeFromPosition(float cx){
        freezeNowTimeUpdate=false;
        nowTime= adsorbNowTime(fromAbsolutePositionToMusicTime(cx));
        handleKeyFrameUpdate();
        handleEventUpdate();
    }

    private void handleKeyFrameUpdate(){
        if (timeLineWidgetUpdateInterface!=null) {
            timeLineWidgetUpdateInterface.onFrameInfoUpdated(getNowLineFrame());
        }
    }

    private void handleEventUpdate(){
        if (timeLineWidgetUpdateInterface!=null){
            TimeLineEvent<?> nowLineEvent = getNowLineEvent();
            if (nowLineEvent instanceof TimeLineMotionEvent timeLineMotionEvent)
                timeLineWidgetUpdateInterface.onMotionEventUpdated(timeLineMotionEvent);
            if (nowLineEvent==null)
                timeLineWidgetUpdateInterface.onMotionEventUpdated(null);
        }
    }

    private void updateScrollPositions(){
        scrollVerticalPosition+=
                (tempScrollVerticalPosition-scrollVerticalPosition)*10f
                        * Gdx.graphics.getDeltaTime();
        scrollHorizontalPosition+=
                (tempScrollHorizontalPosition-scrollHorizontalPosition)*10f
                        * Gdx.graphics.getDeltaTime();
    }

    private void updateScrollFade(){
        scrollVerticalFade+=
                (tempScrollVerticalFade-scrollVerticalFade)*10f
                        * Gdx.graphics.getDeltaTime();
        scrollHorizontalFade+=
                (tempScrollHorizontalFade-scrollHorizontalFade)*10f
                        * Gdx.graphics.getDeltaTime();
    }

    private float beatToSecond(float bpm,float beat){
        return beat/bpm*60f;
    }

    private float secondToBeat(float bpm,float second){
        return bpm*second/60f;
    }

    private float fromAbsolutePositionToMusicTime(float absolutePosition/*widget*/){
        float musicLength = V.project.getMusicLength();
        float leftLength = musicLength * scrollHorizontalPosition;
        float truePos = absolutePosition - leftWidth;
        return truePos/speed+leftLength;
    }

    private float fromMusicTimeToAbsolutePosition(float musicTime){
        float musicLength = V.project.getMusicLength();
        float leftLength = musicLength * scrollHorizontalPosition;
        return (musicTime - leftLength) * speed+leftWidth;
    }


    /*private void updateFrameToParent(){
        Group parent = getParent();
        if(!(parent instanceof LevelEditorDialog))return;
        ((LevelEditorDialog)parent).updateFrameInfo(getNowLineFrame(),0);
    }

    private void updateEventToParent(){
        Group parent = getParent();
        if(!(parent instanceof LevelEditorDialog))return;
        ((LevelEditorDialog)parent).updateEvent(getNowTimeLineLineEvent());
    }

    private void updateKeyFramesToParent() {
        Group parent = getParent();
        if (!(parent instanceof LevelEditorDialog)) return;
        ((LevelEditorDialog) parent).cleanKeyFrames();
    }

    private Frame getNowLineFrame(){
        int trueLine = (int) Math.floor(currentTrackAbs / (float) indexMultiple);
        TimeLineRow row = V.project.getLevelData().getTimeLine().getTimeLineRows().get(trueLine);
        return FrameUtil.getLineNowFrame(V.project.getLevelData().getTimeLine(), nowTime, row.getBindLineNum());
    }

    private String getTimeLineEventSetName(TimeLineEventSet set){
        String[] strings = ReflectUtil.compareType(set.getType());
        if (strings.length==0)
            return "";
        return " - "+strings[0];
    }

    private int checkOtherChildTrackForKeyFrame(Frame frame,TimeLineRow row,int childIndex){
        float time = frame.getTime();
        TimeLineEventSet timeLineEventSet = row.getTimeLineEventSets().get(childIndex);
        List<Frame> keyFrames = timeLineEventSet.getKeyFrames();
        for (int i = 0; i < keyFrames.size(); i++) {
            if (keyFrames.get(i).getTime()==time)return i;
        }
        return -1;
    }*/

    private int getBaseIndex(){
        int cachedShowedItems = timeLineWidgetUtil.getCachedShowedItems();
        return (int)Math.floor((cachedShowedItems+getHeight())*scrollVerticalPosition);
    }

    private void updateSelectIndexFromAbsolutePosition(float cy){
        float unitHeight=(height-scrollBarWide)/ itemsPerScreen;
        float itemTotalHeight = timeLineWidgetUtil.getItemTotalHeight(unitHeight);
        float drawingStartY=height+(itemTotalHeight-height+scrollBarWide)*scrollVerticalPosition+unitHeight;
        this.nowSelection= timeLineWidgetUtil.getSelection(i ->
                drawingStartY - unitHeight * (i-1) >= y+cy && drawingStartY - unitHeight * (i) <= y+cy);
    }

    private void updateLayout(){
        this.x=getX();
        this.y=getY();
        this.width=getWidth();
        this.height=getHeight();
    }

    private void clearKeyFrameUpdates(){
        if (timeLineWidgetUpdateInterface!=null)
            timeLineWidgetUpdateInterface.onKeyFrameUpdated(nowKeyFrame=new FrameGeneral());
    }

    public BaseFrame<?> getNowKeyFrame() {
        return nowKeyFrame;
    }

    public TimeLineWidgetUtil.Selection getNowSelected(){
        return nowSelection;
    }

    public float getNowTime() {
        return nowTime;
    }

    @Override
    public void draw(Batch batch, float parentAlpha) {
        super.draw(batch, parentAlpha);
        updateLayout();
        drawBox();
        drawTimeLine(batch);
        drawRuler(batch);
        //drawSelection();
        drawSelectedLine(batch);
        drawScrollBarAndOutLine();
        drawNowTimeLine();
        updateNowTime();
        updateScrollPositions();
        updateScrollFade();
        updateAltAdsorbChange();




        float lastBeatTime = beatUtil.getNearBackBeatTime(tTime);
        float nextBeatTime = beatUtil.getNearFrontBeatTime(tTime);
        float v1 = fromMusicTimeToAbsolutePosition(lastBeatTime);
        float v2 = fromMusicTimeToAbsolutePosition(nextBeatTime);
        shapeDrawer.line(x+v1,y,x+v1,y+height-50,Color.BROWN,5);
        shapeDrawer.line(x+v2,y,x+v2,y+height,Color.GREEN,5);
    }

    private enum AdsorbMode{
        None,Frame,Beat,Note
    }

    private enum RulerType{
        None,Beat
    }

    public interface TimeLineWidgetUpdateInterface {
        void onFrameInfoUpdated(BaseFrame<?> frame);
        void onKeyFrameUpdated(BaseFrame<?> frame);
        void onMotionEventUpdated(TimeLineMotionEvent timeLineMotionEvent);
    }
}
